Improve performance by rendering projects view all at once#153
Improve performance by rendering projects view all at once#153dwilding merged 4 commits intocanonical:next-releasefrom
Conversation
Previously, the project list was lazy-loaded with htmx. This update restores the bulk delivery of the page, along with opportuities for substantially reducing the number of per-row queries. With luck, this should turn out to be fast enough in practice that we can keep this approach.
4acce36 to
2849585
Compare
|
I've rebased this onto |
There was a problem hiding this comment.
Pull request overview
This PR aims to improve the performance of the project list page by reducing per-row ORM work and removing HTMX-based lazy row loading in favor of server-side rendering with precomputed data.
Changes:
- Optimize
ProjectListViewqueryset withselect_related/prefetch_relatedand precompute per-project objective/QI data in bulk. - Remove the
project_rowendpoint and HTMX “intersect” row loading, rendering rows directly via a template include. - Add a regression test ensuring QI history, current QI, and objective levels render on the project list page.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
dashboard/projects/views.py |
Adds eager-loading and bulk aggregation/mapping to reduce ORM work on the list view; removes project_row view. |
dashboard/projects/urls.py |
Removes the project_row route since rows are no longer lazily fetched. |
dashboard/projects/test_views.py |
Adds coverage for rendering QI history/current QI and levels in the list page. |
dashboard/projects/templates/projects/project_list.html |
Replaces HTMX lazy-loading row placeholder with direct include of the row partial. |
dashboard/projects/templates/projects/partial_project_list_row.html |
Switches template to use the new precomputed attributes for QI history and QI total. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Build a per-project objective map once to avoid repeated row-level lookups. | ||
| pos_by_project = {} | ||
| for po in ProjectObjective.objects.all().values( | ||
| "project_id", | ||
| "objective__name", | ||
| "id", | ||
| "level_achieved__name", | ||
| "unstarted_reason__name", | ||
| ): | ||
| pos_by_project.setdefault(po["project_id"], []).append(po) |
There was a problem hiding this comment.
pos_by_project is built from ProjectObjective.objects.all() and without any ordering. This will load objectives for every project in the database (hurting performance on large datasets) and may render objective columns in an arbitrary order, misaligning cells with objective_list (which is ordered by Objective.Meta.ordering). Build projects/project_ids first, then query ProjectObjective with project_id__in=project_ids and an explicit order_by that matches the objective header order (e.g., objective group/name or objective_id).
There was a problem hiding this comment.
This is a good comment, but we can disregard it:
- Ordering - Per docs, Django will use the default ordering on the related model, in this case following our specified ordering of Project and Objective objects.
- Performance - We want to load ProjectObjectives for every project, because we're rendering the all-projects view.
dwilding
left a comment
There was a problem hiding this comment.
I did some additional local testing to check that the rendering and new test work as expected.
Depends on #150; merge that first